#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics;
using C5;

namespace Grawlix.Services.Resources
{
	/// <summary>
	/// A cache that connects different <see cref="AppDomain"/>
	/// repositories.
	/// </summary>
	/// <typeparam name="TDeclare">The type of resource that this
	/// class declares it caches.
	/// </typeparam>
	/// <typeparam name="TDefine">The type of resource that this
	/// class actually caches.
	/// </typeparam>
	[Serializable]
	internal class ResourceCache<TDeclare, TDefine>:
		PermanentMarshalByRefObject
		where TDeclare : IDynamicResource
		where TDefine : AbstractResourceProxy<TDeclare, TDefine>,
			TDeclare
	{
		/// <summary>
		/// The data stored within the cache.
		/// </summary>
		private readonly HashDictionary<Uri, WeakReference> _cache
			= new HashDictionary<Uri, WeakReference>();

		/// <summary>
		/// The resource creator object.
		/// </summary>
		private readonly
			AbstractResourceProxyCreator<TDeclare, TDefine> _creator;

		/// <summary>
		/// The backing repository for the resource.
		/// </summary>
		private readonly ResourceRepository<TDeclare, TDefine>
			_repository;

		/// <summary>
		/// Initializes the cache.
		/// </summary>
		/// <param name="repository">The base repository that is being
		/// cached.
		/// </param>
		/// <exception cref="InvalidOperationException">
		/// <paramref name="repository"/> is null.</exception>
		/// <exception cref="ArgumentNullException"><c>repository</c>is
		/// null.</exception>
		internal ResourceCache(
			ResourceRepository<TDeclare, TDefine> repository)
		{
			if (repository == null)
				throw new ArgumentNullException("repository");
			_repository = repository;
			_creator = _repository.Creator;
			if (_creator == null)
			{
				throw new InvalidOperationException(
					"The AbstractResourceProxyCreator<" +
					typeof(TDeclare).Name + "> given to " + this +
					" is null.");
			}
		}

		/// <summary>
		/// Gets the backing repository.
		/// </summary>
		/// <value>The backing repository.</value>
		public ResourceRepository<TDeclare, TDefine> Repository
		{
			get { return _repository; }
		}

		/// <summary>
		/// Finds the resource with the specified identifier.
		/// </summary>
		/// <param name="identifier">The identifier of the resource.
		/// </param>
		/// <returns>The located resource.</returns>
		public TDefine Find(Uri identifier)
		{
			return MarshalResource(identifier);
		}

		/// <summary>
		/// Pulls a resource from the cache or requests one from the
		/// repository.
		/// </summary>
		/// <param name="identifier">The URI of the resource.</param>
		/// <returns>The marshaled resource.</returns>
		/// <exception cref="ArgumentNullException"><c>identifier</c>
		/// is null.</exception>
		/// <exception cref="InvalidOperationException">The specified
		/// repository holds the wrong type of resource.</exception>
		public TDefine MarshalResource(Uri identifier)
		{
			TDefine resource;
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			lock (_cache)
			{
				resource = TryGetResource(identifier);
				if (resource != null)
					return resource;
				resource = _creator.Create(identifier);
				if (resource == null)
				{
					throw new InvalidOperationException(
						"The AbstractResourceProxyCreator<" +
						typeof(TDeclare).Name + "> " + "provided to " + this +
						" does not " + "create objects of type " +
						"AbstractResourceProxy<" + typeof(TDeclare).Name +
						">.");
				}
				_cache[identifier] = new WeakReference(resource);
				resource.Attach(this);
			}
			_repository.Update(AppDomain.CurrentDomain, this,
			                   identifier);
			return resource;
		}

		/// <summary>
		/// Removes the specified resource.
		/// </summary>
		/// <param name="resource">The finalized resource being
		/// removed.</param>
		/// <exception cref="ArgumentNullException"><c>resource</c> is
		/// null.</exception>
		public void RemoveResource(
			AbstractResourceProxy<TDeclare, TDefine> resource)
		{
			if (resource == null)
				throw new ArgumentNullException("resource");
			Uri identifier = resource.Identifier;
			lock (_cache)
			{
				if (!_cache.Contains(identifier))
					return;
				object data = _cache[identifier].Target;
				// During closing, data can be resource
				if ((data != null) && (data != resource))
					return;
				_cache.Remove(identifier);
			}
			// Dance a little to keep the lock order correct
			_repository.Remove(AppDomain.CurrentDomain, identifier);
		}

		/// <summary>
		/// Checks to see if a resource is present.
		/// </summary>
		/// <param name="identifier">The URI of the resource.</param>
		/// <returns>true if the resource is present, else false.
		/// </returns>
		/// <exception cref="ArgumentNullException"><c>identifier</c>
		/// is null.</exception>
		public bool ResourcePresent(Uri identifier)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			lock (_cache)
			{
				return _cache.Contains(identifier);
			}
		}

		/// <summary>
		/// Gets the specified resource or returns null.
		/// </summary>
		/// <param name="identifier">The identifier of the resource.
		/// </param>
		/// <returns>The resource, or null on failure.</returns>
		private TDefine TryGetResource(Uri identifier)
		{
			if (_cache.Contains(identifier))
			{
				object target = _cache[identifier].Target;
				var typedTarget = target as TDefine;
				// Valid for it to be null.
				Debug.Assert(target == typedTarget);
				return typedTarget;
			}
			return null;
		}

		/// <summary>
		/// Updates the state of the specified resource.
		/// </summary>
		/// <param name="identifier">The URI of the resource to update.
		/// </param>
		/// <param name="state">The state to update.</param>
		/// <exception cref="ArgumentNullException"><c>identifier</c>
		/// is null.</exception>
		public void UpdateState(Uri identifier,
		                        ResourceState state)
		{
			if (identifier == null)
				throw new ArgumentNullException("identifier");
			lock (_cache)
			{
				AbstractResourceProxy<TDeclare, TDefine> resource =
					TryGetResource(identifier);
				if (resource != null)
					resource.SetState(state);
			}
		}
	}
}